bookwiz.io / app / api / books / [id] / github-integration / diff / route.ts
route.ts
Raw
import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase'

export async function POST(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const supabase = createServerSupabaseClient()
    const bookId = params.id
    const { currentFiles } = await request.json()
    
    // Get current user session
    const authHeader = request.headers.get('authorization')
    let user
    
    try {
      if (authHeader) {
        const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', ''))
        if (!authError) user = authUser
      } else {
        const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser()
        if (!sessionError) user = sessionUser
      }
    } catch (e) {
      // Ignore auth errors
    }

    if (!user) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      )
    }

    // Get GitHub integration
    const { data: profile } = await supabase
      .from('profiles')
      .select('github_integrations')
      .eq('id', user.id)
      .single()

    const integration = profile?.github_integrations?.[bookId]
    if (!integration) {
      return NextResponse.json(
        { error: 'GitHub integration not found' },
        { status: 404 }
      )
    }

    const owner = integration.github_username
    const repo = integration.repository_name
    const accessToken = integration.access_token

    try {
      // Get the diff between current files and the latest commit
      const diffResult = await generateGitStyleDiff(
        owner, 
        repo, 
        accessToken, 
        currentFiles
      )
      
      return NextResponse.json({
        changes: diffResult.changes,
        summary: diffResult.summary,
        diffText: diffResult.diffText
      })
    } catch (githubError) {
      console.error('โŒ GitHub Diff API: Error generating diff:', githubError)
      return NextResponse.json(
        { error: 'Failed to generate diff', details: githubError instanceof Error ? githubError.message : String(githubError) },
        { status: 500 }
      )
    }

  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

// Helper function to generate git-style diff
async function generateGitStyleDiff(
  owner: string, 
  repo: string, 
  accessToken: string, 
  currentFiles: Array<{ name: string; content: string; path: string }>
): Promise<{
  changes: Array<{
    path: string
    changeType: 'added' | 'modified' | 'deleted'
    linesAdded: number
    linesRemoved: number
    currentContent: string
    committedContent: string
  }>
  summary: {
    filesChanged: number
    insertions: number
    deletions: number
  }
  diffText: string
}> {
  console.log('๐Ÿ“Š GitHub Diff API: Generating git-style diff')
  
  // Get the latest commit SHA
  const latestCommitResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/HEAD`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Accept': 'application/vnd.github.v3+json'
    }
  })

  if (!latestCommitResponse.ok) {
    if (latestCommitResponse.status === 409) {
      // Repository is empty - all files are new
      const changes = currentFiles.map(file => ({
        path: file.path,
        changeType: 'added' as const,
        linesAdded: file.content.split('\n').length,
        linesRemoved: 0,
        currentContent: file.content,
        committedContent: ''
      }))
      
      const summary = {
        filesChanged: changes.length,
        insertions: changes.reduce((sum, change) => sum + change.linesAdded, 0),
        deletions: 0
      }
      
      return { changes, summary, diffText: generateDiffText(changes) }
    }
    throw new Error(`Failed to get latest commit: ${latestCommitResponse.status}`)
  }

  const latestCommit = await latestCommitResponse.json()
  
  // Get the tree for the latest commit
  const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${latestCommit.sha}?recursive=1`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Accept': 'application/vnd.github.v3+json'
    }
  })

  if (!treeResponse.ok) {
    throw new Error(`Failed to get repository tree: ${treeResponse.status}`)
  }

  const treeData = await treeResponse.json()
  const committedFiles: { [path: string]: string } = {}

  // Get content for all committed files
  const fileBlobs = treeData.tree.filter((item: any) => 
    item.type === 'blob' && 
    item.path !== 'README.md' &&
    !item.path.startsWith('.git')
  )

  console.log(`๐Ÿ“Š GitHub Diff API: Processing ${fileBlobs.length} committed files`)

  // Fetch committed file contents in batches
  const batchSize = 10
  for (let i = 0; i < fileBlobs.length; i += batchSize) {
    const batch = fileBlobs.slice(i, i + batchSize)
    
    await Promise.all(batch.map(async (blob: any) => {
      try {
        const blobResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`, {
          headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Accept': 'application/vnd.github.v3+json'
          }
        })
        
        if (blobResponse.ok) {
          const blobData = await blobResponse.json()
          
          let content = ''
          if (blobData.encoding === 'base64') {
            try {
              content = Buffer.from(blobData.content, 'base64').toString('utf-8')
            } catch {
              // Skip binary files
              return
            }
          } else {
            content = blobData.content
          }
          
          committedFiles[blob.path] = content
        }
      } catch (error) {
        console.error(`โŒ GitHub Diff API: Error fetching blob ${blob.path}:`, error)
      }
    }))
  }

  // Compare current files with committed files
  const changes: Array<{
    path: string
    changeType: 'added' | 'modified' | 'deleted'
    linesAdded: number
    linesRemoved: number
    currentContent: string
    committedContent: string
  }> = []

  // Check for added and modified files
  currentFiles.forEach(currentFile => {
    const committedContent = committedFiles[currentFile.path] || ''
    
    if (currentFile.content !== committedContent) {
      const currentLines = currentFile.content.split('\n')
      const committedLines = committedContent.split('\n')
      
      changes.push({
        path: currentFile.path,
        changeType: committedContent ? 'modified' : 'added',
        linesAdded: Math.max(0, currentLines.length - committedLines.length),
        linesRemoved: Math.max(0, committedLines.length - currentLines.length),
        currentContent: currentFile.content,
        committedContent
      })
    }
  })

  // Check for deleted files
  Object.keys(committedFiles).forEach(committedPath => {
    const currentFileExists = currentFiles.some(f => f.path === committedPath)
    
    if (!currentFileExists) {
      const committedContent = committedFiles[committedPath]
      const committedLines = committedContent.split('\n')
      
      changes.push({
        path: committedPath,
        changeType: 'deleted',
        linesAdded: 0,
        linesRemoved: committedLines.length,
        currentContent: '',
        committedContent
      })
    }
  })

  const summary = {
    filesChanged: changes.length,
    insertions: changes.reduce((sum, change) => sum + change.linesAdded, 0),
    deletions: changes.reduce((sum, change) => sum + change.linesRemoved, 0)
  }

  return {
    changes,
    summary,
    diffText: generateDiffText(changes)
  }
}

// Helper function to generate unified diff text
function generateDiffText(changes: Array<{
  path: string
  changeType: 'added' | 'modified' | 'deleted'
  currentContent: string
  committedContent: string
}>): string {
  let diffText = ''
  
  changes.forEach(change => {
    diffText += `\n--- a/${change.path}\n+++ b/${change.path}\n`
    
    if (change.changeType === 'added') {
      const lines = change.currentContent.split('\n')
      diffText += `@@ -0,0 +1,${lines.length} @@\n`
      lines.forEach(line => {
        diffText += `+${line}\n`
      })
    } else if (change.changeType === 'deleted') {
      const lines = change.committedContent.split('\n')
      diffText += `@@ -1,${lines.length} +0,0 @@\n`
      lines.forEach(line => {
        diffText += `-${line}\n`
      })
    } else {
      // Modified file - generate a simple unified diff
      const oldLines = change.committedContent.split('\n')
      const newLines = change.currentContent.split('\n')
      
      diffText += `@@ -1,${oldLines.length} +1,${newLines.length} @@\n`
      
      // Simple line-by-line comparison (not optimal but works)
      const maxLines = Math.max(oldLines.length, newLines.length)
      for (let i = 0; i < maxLines; i++) {
        const oldLine = oldLines[i] || ''
        const newLine = newLines[i] || ''
        
        if (oldLine !== newLine) {
          if (oldLines[i] !== undefined) diffText += `-${oldLine}\n`
          if (newLines[i] !== undefined) diffText += `+${newLine}\n`
        } else {
          diffText += ` ${oldLine}\n`
        }
      }
    }
  })
  
  return diffText
}